import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import * as Sentry from '@sentry/browser';
import { GlLink, GlIcon, GlSkeletonLoader } from '@gitlab/ui';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import VulnerabilityTraining, {
  i18n,
  TRAINING_URL_POLLING_INTERVAL,
} from 'ee/vulnerabilities/components/vulnerability_training.vue';
import securityTrainingProvidersQuery from '~/security_configuration/graphql/security_training_providers.query.graphql';
import securityTrainingVulnerabilityQuery from '~/security_configuration/graphql/security_training_vulnerability.query.graphql';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import {
  SECURITY_TRAINING_URL_STATUS_PENDING,
  SUPPORTED_IDENTIFIER_TYPE_CWE,
} from 'ee/vulnerabilities/constants';
import {
  TRACK_CLICK_TRAINING_LINK_ACTION,
  TRACK_TRAINING_LOADED_ACTION,
} from '~/security_configuration/constants';
import { formatIdentifierExternalIds } from 'ee/vulnerabilities/helpers';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import {
  testProviderName,
  testTrainingUrls,
  getSecurityTrainingProvidersData,
} from 'jest/security_configuration/mock_data';
import { getSecurityTrainingProjectData, testIdentifiers, testIdentifierName } from './mock_data';

Vue.use(VueApollo);

const projectFullPath = 'namespace/project';

const TEST_TRAINING_PROVIDERS_ALL_DISABLED = getSecurityTrainingProvidersData();
const TEST_TRAINING_PROVIDERS_FIRST_ENABLED = getSecurityTrainingProvidersData({
  providerOverrides: { first: { isEnabled: true } },
});
const TEST_TRAINING_PROVIDERS_DEFAULT = TEST_TRAINING_PROVIDERS_FIRST_ENABLED;
const TEST_TRAINING_URLS_DEFAULT = getSecurityTrainingProjectData();
const TEST_TRAINING_URLS_EMPTY = getSecurityTrainingProjectData({ urls: [] });
const TEMP_PROVIDER_LOGOS = {
  Kontra: {
    svg: '<svg>Kontra</svg>',
  },
  'Secure Code Warrior': {
    svg: '<svg>Secure Code Warrior</svg>',
  },
  SecureFlag: {
    svg: '<svg>SecureFlag</svg>',
  },
};

jest.mock('~/security_configuration/components/constants', () => {
  return {
    TEMP_PROVIDER_URLS: jest.requireActual('~/security_configuration/components/constants')
      .TEMP_PROVIDER_URLS,
    // NOTE: Jest hoists all mocks to the top so we can't use TEMP_PROVIDER_LOGOS
    // here directly.
    TEMP_PROVIDER_LOGOS: {
      Kontra: {
        svg: '<svg>Kontra</svg>',
      },
      'Secure Code Warrior': {
        svg: '<svg>Secure Code Warrior</svg>',
      },
      SecureFlag: {
        svg: '<svg>SecureFlag</svg>',
      },
    },
  };
});

describe('VulnerabilityTraining component', () => {
  let wrapper;
  let apolloProvider;

  const createApolloProvider = ({ providersQueryHandler, projectQueryHandler } = {}) => {
    apolloProvider = createMockApollo([
      [
        securityTrainingProvidersQuery,
        providersQueryHandler ||
          jest.fn().mockResolvedValue(TEST_TRAINING_PROVIDERS_DEFAULT.response),
      ],
      [
        securityTrainingVulnerabilityQuery,
        projectQueryHandler || jest.fn().mockResolvedValue(TEST_TRAINING_URLS_DEFAULT.response),
      ],
    ]);
  };

  const createComponent = (props = {}, { slots = {} } = {}) => {
    wrapper = shallowMountExtended(VulnerabilityTraining, {
      propsData: {
        projectFullPath,
        identifiers: testIdentifiers,
        file: 'some/file.js',
        ...props,
      },
      slots,
      apolloProvider,
    });
  };

  beforeEach(() => {
    createApolloProvider();
  });

  afterEach(() => {
    apolloProvider = null;
  });

  const waitForQueryToBeLoaded = () => waitForPromises();
  const findUnavailableMessage = () => wrapper.findByTestId('unavailable-message');
  const findTrainingItemName = (name) => wrapper.findByText(name);
  const findTrainingItemLinks = () => wrapper.findAllComponents(GlLink);
  const findTrainingItemLinkIcons = () => wrapper.findAllComponents(GlIcon);
  const findTrainingLogos = () => wrapper.findAllByTestId('provider-logo');

  describe('with the query being successful', () => {
    describe('basic structure', () => {
      it('does not render component when there are no enabled securityTrainingProviders', async () => {
        createApolloProvider({
          providersQueryHandler: jest
            .fn()
            .mockResolvedValue(TEST_TRAINING_PROVIDERS_ALL_DISABLED.response),
        });
        createComponent();
        await waitForQueryToBeLoaded();

        expect(wrapper.html()).toBe('');
      });

      it('watches showVulnerabilityTraining and emits change', async () => {
        createApolloProvider();
        createComponent();

        await waitForQueryToBeLoaded();
        await nextTick();

        // Note: the event emits twice - the second time is when the query is loaded
        expect(wrapper.emitted('show-vulnerability-training')).toEqual([[false], [true]]);
      });
    });

    describe('with title slot', () => {
      it('renders slot content', async () => {
        const mockSlotText = 'some title';
        createComponent({}, { slots: { header: mockSlotText } });
        await waitForQueryToBeLoaded();
        expect(wrapper.text()).toContain(mockSlotText);
      });
    });

    describe('training availability message', () => {
      it('displays message when there are no supported identifier', async () => {
        createComponent({ identifiers: [{ externalType: 'not supported identifier' }] });
        await waitForQueryToBeLoaded();
        expect(findUnavailableMessage().text()).toBe(i18n.trainingUnavailable);
      });

      it('displays message when there are no security training urls', async () => {
        createApolloProvider({
          projectQueryHandler: jest.fn().mockResolvedValue(TEST_TRAINING_URLS_EMPTY.response),
        });
        createComponent();
        await waitForQueryToBeLoaded();

        expect(findUnavailableMessage().exists()).toBe(true);
      });

      it.each`
        identifier                                     | exists
        ${SUPPORTED_IDENTIFIER_TYPE_CWE.toUpperCase()} | ${false}
        ${SUPPORTED_IDENTIFIER_TYPE_CWE.toLowerCase()} | ${false}
        ${'non-supported-identifier'}                  | ${true}
      `('sets it to "$exists" for "$identifier"', async ({ identifier, exists }) => {
        createApolloProvider();
        createComponent({ identifiers: [{ externalType: identifier, name: testIdentifierName }] });
        await waitForQueryToBeLoaded();
        expect(findUnavailableMessage().exists()).toBe(exists);
      });
    });

    describe('GlSkeletonLoader', () => {
      it('displays when there are supported identifiers and some urls are in pending status', async () => {
        createApolloProvider({
          projectQueryHandler: jest.fn().mockResolvedValue(
            getSecurityTrainingProjectData({
              urlOverrides: {
                first: {
                  status: SECURITY_TRAINING_URL_STATUS_PENDING,
                },
              },
            }).response,
          ),
        });
        createComponent();
        await waitForQueryToBeLoaded();

        expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true);
      });
    });

    describe('polling', () => {
      let apolloQuery;

      beforeEach(() => {
        createApolloProvider();
        createComponent();
        apolloQuery = wrapper.vm.$apollo.queries.securityTrainingUrls;
        jest.spyOn(apolloQuery, 'stopPolling').mockImplementation(jest.fn());
      });

      it(`sets polling at ${TRAINING_URL_POLLING_INTERVAL} ms`, () => {
        expect(apolloQuery.options.pollInterval).toBe(TRAINING_URL_POLLING_INTERVAL);
      });

      it('stops polling when every training url status is completed', async () => {
        await waitForQueryToBeLoaded();
        await nextTick();

        expect(apolloQuery.stopPolling).toHaveBeenCalled();
      });
    });

    describe('training logo', () => {
      beforeEach(async () => {
        createApolloProvider();
        createComponent();
        await waitForQueryToBeLoaded();
      });

      const providerIndexArray = [0, 1];

      it.each(providerIndexArray)('displays the correct width for training item %s', (index) => {
        expect(findTrainingLogos().at(index).attributes('style')).toBe('width: 12px;');
      });

      it.each(providerIndexArray)('has a11y decorative attribute for provider %s', (index) => {
        expect(findTrainingLogos().at(index).attributes('role')).toBe('presentation');
      });

      it.each(providerIndexArray)('renders the svg content for training item %s', (index) => {
        expect(findTrainingLogos().at(index).html()).toContain(
          TEMP_PROVIDER_LOGOS[testProviderName[index]].svg,
        );
      });
    });

    describe('training item', () => {
      it('displays correct number of training items', async () => {
        createApolloProvider();
        createComponent();
        await waitForQueryToBeLoaded();

        expect(findTrainingItemLinks()).toHaveLength(testTrainingUrls.length);
      });

      it.each([0, 1])('displays training item %s', async (index) => {
        createApolloProvider();
        createComponent();
        await waitForQueryToBeLoaded();

        expect(findTrainingItemName(testProviderName[index]).exists()).toBe(true);
        expect(findTrainingItemLinks().at(index).attributes('href')).toBe(testTrainingUrls[index]);
        expect(findTrainingItemLinkIcons().at(index).attributes('name')).toBe('external-link');
      });

      it('does not display training item if there are no securityTrainingUrls', async () => {
        createApolloProvider({
          projectQueryHandler: jest.fn().mockResolvedValue(TEST_TRAINING_URLS_EMPTY.response),
        });
        createComponent();
        await waitForQueryToBeLoaded();

        expect(findTrainingItemLinks().exists()).toBe(false);
        expect(findTrainingItemLinkIcons().exists()).toBe(false);
      });
    });

    describe('security training query', () => {
      const projectQueryHandler = jest.fn().mockResolvedValue(TEST_TRAINING_URLS_EMPTY.response);
      const identifiers = [
        {
          externalType: SUPPORTED_IDENTIFIER_TYPE_CWE,
          externalId: 'some external id',
          name: testIdentifierName,
        },
      ];

      beforeEach(() => {
        createApolloProvider({ projectQueryHandler });
      });

      it('is called with the correct variables', async () => {
        const file = 'some/file.js';
        const { externalType, externalId, name } = identifiers[0];

        createComponent({ identifiers, file });
        await waitForQueryToBeLoaded();

        expect(projectQueryHandler).toHaveBeenCalledWith(
          expect.objectContaining({
            identifierExternalIds: [
              formatIdentifierExternalIds({ externalType, externalId, name }),
            ],
            projectFullPath,
            filename: file,
          }),
        );
      });

      it('does not pass filename parameter for default null value', async () => {
        const file = null;
        createComponent({ identifiers });

        await waitForQueryToBeLoaded();

        expect(projectQueryHandler).toHaveBeenCalledWith(
          expect.not.objectContaining({
            filename: file,
          }),
        );
      });

      describe('identifierExternalIds', () => {
        const externalId = 'random id';
        const name = 'random name';
        const supportedExternalTypes = ['CWE', 'cwe', 'owasp'];
        const unsupportedExternalTypes = ['cve', 'OWASP'];
        const mixExternalTypes = [...supportedExternalTypes, ...unsupportedExternalTypes];
        const mixIdentifiers = mixExternalTypes.map((type) => ({
          externalType: type,
          externalId,
          name,
        }));

        beforeEach(() => {
          createComponent({ identifiers: mixIdentifiers });
        });

        it('is called with supported identifierExternalIds', async () => {
          const supportedIdentifierExternalIds = supportedExternalTypes.map((externalType) =>
            formatIdentifierExternalIds({ externalType, externalId, name }),
          );

          await waitForQueryToBeLoaded();

          expect(projectQueryHandler).toHaveBeenCalledWith(
            expect.objectContaining({
              identifierExternalIds: expect.arrayContaining(supportedIdentifierExternalIds),
            }),
          );
        });

        it('is not called with unsupported identifierExternalIds', () => {
          const unsupportedIdentifierExternalIds = unsupportedExternalTypes.map((externalType) =>
            formatIdentifierExternalIds({ externalType, externalId, name }),
          );
          expect(projectQueryHandler).toHaveBeenCalledWith(
            expect.objectContaining({
              identifierExternalIds: expect.not.arrayContaining(unsupportedIdentifierExternalIds),
            }),
          );
        });
      });
    });
  });

  describe('with the query resulting in an error', () => {
    it('reports the error to sentry', async () => {
      jest.spyOn(Sentry, 'captureException');
      createApolloProvider({ providersQueryHandler: jest.fn().mockResolvedValue(new Error()) });
      createComponent();

      expect(Sentry.captureException).not.toHaveBeenCalled();

      await waitForQueryToBeLoaded();

      expect(Sentry.captureException).toHaveBeenCalled();
    });
  });

  describe('metrics', () => {
    let trackingSpy;

    beforeEach(() => {
      trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
    });

    afterEach(() => {
      unmockTracking();
    });

    const expectedTrackingOptions = (index) => ({
      label: `vendor_${testProviderName[index]}`,
      property: projectFullPath,
      extra: {
        vulnerability: testIdentifierName,
      },
    });

    it('tracks when the training urls are first loaded', async () => {
      const projectQueryHandler = jest.fn().mockResolvedValue(
        getSecurityTrainingProjectData({
          urlOverrides: {
            first: {
              status: SECURITY_TRAINING_URL_STATUS_PENDING,
            },
          },
        }).response,
      );

      expect(trackingSpy).not.toHaveBeenCalled();

      createApolloProvider({
        projectQueryHandler,
      });
      createComponent();
      await waitForQueryToBeLoaded();

      // after the first loading of the urls, the tracking should be called
      expect(trackingSpy).toHaveBeenCalledWith(undefined, TRACK_TRAINING_LOADED_ACTION, {
        property: projectFullPath,
      });

      // fake a poll-cycle
      jest.advanceTimersByTime(TRAINING_URL_POLLING_INTERVAL);
      await waitForQueryToBeLoaded();

      // make sure that we queried twice
      expect(projectQueryHandler).toHaveBeenCalledTimes(2);
      expect(trackingSpy).toHaveBeenCalledTimes(1);
    });

    it.each([0, 1, 2])('tracks when training link %s gets clicked', async (index) => {
      createApolloProvider();
      createComponent();
      await waitForQueryToBeLoaded();
      await findTrainingItemLinks().at(index).vm.$emit('click');

      expect(trackingSpy).toHaveBeenCalledWith(
        undefined,
        TRACK_CLICK_TRAINING_LINK_ACTION,
        expectedTrackingOptions(index),
      );
    });
  });
});
